package crmdna.program;

import static crmdna.common.OfyService.ofy;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Ref;
import com.googlecode.objectify.cmd.Query;

import crmdna.client.Client;
import crmdna.common.APIException;
import crmdna.common.APIResponse.Status;
import crmdna.common.DateUtils;
import crmdna.common.Utils;
import crmdna.common.Utils.Currency;
import crmdna.group.Group;
import crmdna.group.GroupEntity;
import crmdna.programtype.ProgramType;
import crmdna.programtype.ProgramTypeEntity;
import crmdna.sequence.Sequence;
import crmdna.sequence.Sequence.SequenceType;
import crmdna.teacher.Teacher;
import crmdna.teacher.TeacherEntity;
import crmdna.user.User;
import crmdna.user.User.Action;
import crmdna.user.User.ResourceType;
import crmdna.venue.Venue;
import crmdna.venue.VenueEntity;

public class Program {

	public static ProgramProp create(String client, long groupId,
			long programTypeId, long venueId, long teacherId,
			int startYYYYMMDD, int endYYYYMMDD, int numBatches,
			String description, double fees, Currency ccy, String login) {

		Client.ensureValid(client);
		User.ensureAccess(client, login, ResourceType.PROGRAM, groupId,
				Action.WRITE);

		GroupEntity groupEntity = Group.safeGet(client, groupId);
		ProgramTypeEntity programTypeEntity = ProgramType.safeGet(client,
				programTypeId);
		VenueEntity venueEntity = Venue.safeGet(client, venueId);
		TeacherEntity teacherEntity = Teacher.safeGet(client, teacherId);

		ProgramEntity programEntity = new ProgramEntity();
		programEntity.programTypeRef = Ref.create(programTypeEntity);
		programEntity.venueRef = Ref.create(venueEntity);
		programEntity.groupRef = Ref.create(groupEntity);
		programEntity.teacherRef = Ref.create(teacherEntity);
		programEntity.startYYYYMMDD = startYYYYMMDD;
		programEntity.endYYYYMMDD = endYYYYMMDD;
		programEntity.numBatches = numBatches;
		programEntity.description = description;
		programEntity.fee = fees;
		programEntity.ccy = ccy;

		ensureValid(programEntity);

		ensureNotPresentInDB(client, programEntity);
		safeAddToMemcache(client, programEntity);

		programEntity.programId = Sequence
				.getNext(client, SequenceType.PROGRAM);

		ofy(client).save().entity(programEntity).now();
		return programEntity.toProp();
	}

	public static ProgramProp setSpecialInstruction(final String client,
			final long programId, final String specialInstruction) {
		Client.ensureValid(client);

		ProgramEntity programEntity = safeGet(client, programId);

		// can specify null to preserve the existing special instruction
		if (null == specialInstruction)
			return programEntity.toProp();

		programEntity.specialInstruction = specialInstruction;

		// Note: there is a minute change of race condition when saving
		ofy(client).save().entity(programEntity).now();
		return programEntity.toProp();
	}

	public static ProgramProp setSessionTimings(final String client, long programId,
			List<String> batch1SessionTimings,
			List<String> batch2SessionTimings,
			List<String> batch3SessionTimings,
			List<String> batch4SessionTimings, List<String> batch5SessionTimings) {

		Client.ensureValid(client);

		// Note: There is a tiny chance of race condition when multiple requests
		// update
		// program entity in the same time though it is likely to be extremely
		// rare as program entities
		// are not updated frequently. Currently we are unable to wrap this in
		// an objectify transaction
		// as loading program entity touches multiple entities and we hit the
		// cross group transaction entity limit

		ProgramEntity programEntity = safeGet(client, programId);

		if (programEntity.numBatches == 1) {
			batch2SessionTimings = null;
			batch3SessionTimings = null;
			batch4SessionTimings = null;
			batch5SessionTimings = null;
		} else if (programEntity.numBatches == 2) {
			batch3SessionTimings = null;
			batch4SessionTimings = null;
			batch5SessionTimings = null;
		} else if (programEntity.numBatches == 3) {
			batch4SessionTimings = null;
			batch5SessionTimings = null;
		} else if (programEntity.numBatches == 4) {
			batch5SessionTimings = null;
		}

		boolean changed = false;
		// can specify null to preserve existing value
		if (batch1SessionTimings != null) {
			programEntity.batch1SessionTimings = batch1SessionTimings;
			changed = true;
		}

		if (batch2SessionTimings != null) {
			programEntity.batch2SessionTimings = batch2SessionTimings;
			changed = true;
		}

		if (batch3SessionTimings != null) {
			programEntity.batch3SessionTimings = batch3SessionTimings;
			changed = true;
		}

		if (batch4SessionTimings != null) {
			programEntity.batch4SessionTimings = batch4SessionTimings;
			changed = true;
		}

		if (batch5SessionTimings != null) {
			programEntity.batch5SessionTimings = batch5SessionTimings;
			changed = true;
		}

		if (!changed)
			return programEntity.toProp();

		ofy(client).save().entity(programEntity).now();
		return programEntity.toProp();
	}

	public static Map<Long, ProgramProp> get(String client,
			Iterable<Long> programIds) {

		Client.ensureValid(client);

		Map<Long, ProgramEntity> programEntities = ofy(client).load()
				.type(ProgramEntity.class).ids(programIds);

		Map<Long, ProgramProp> programProps = new HashMap<>();

		for (Long programId : programEntities.keySet()) {
			ProgramEntity programEntity = programEntities.get(programId);

			ProgramProp programProp = null;
			if (programEntity != null)
				programProp = programEntity.toProp();

			programProps.put(programId, programProp);
		}

		return programProps;
	}

	public static ProgramProp update(String client, long programId,
			Long newVenueId, Long newTeacherId, Integer newStartYYYYMMDD,
			Integer newEndYYYYMMDD, Integer newNumBatches,
			String newDescription, Double fees, Currency ccy, String login) {

		// warning: this method will modify session cache if an exception is
		// thrown

		Client.ensureValid(client);
		ProgramEntity programEntity = safeGet(client, programId);
		ProgramProp programProp = programEntity.toProp();

		User.ensureAccess(client, login, ResourceType.PROGRAM,
				programEntity.groupRef.get().toProp().groupId, Action.WRITE);

		boolean checkUnique = false;
		if (null != newVenueId) {
			if (newVenueId != programProp.venueProp.venueId) {
				VenueEntity venueEntity = Venue.safeGet(client, newVenueId);
				programEntity.venueRef = Ref.create(venueEntity);
				checkUnique = true;
			}
		}

		if (null != newTeacherId) {
			if (newTeacherId != programProp.teacherProp.teacherId) {
				TeacherEntity teacherEntity = Teacher.safeGet(client,
						newTeacherId);
				programEntity.teacherRef = Ref.create(teacherEntity);
			}
		}

		if (null != newStartYYYYMMDD) {
			if (newStartYYYYMMDD != programProp.startYYYYMMDD) {
				DateUtils.ensureFormatYYYYMMDD(newStartYYYYMMDD);
				programEntity.startYYYYMMDD = newStartYYYYMMDD;
				checkUnique = true;
			}
		}

		if (null != newEndYYYYMMDD) {
			if (newEndYYYYMMDD != programProp.endYYYYMMDD) {
				DateUtils.ensureFormatYYYYMMDD(newEndYYYYMMDD);
				programEntity.endYYYYMMDD = newEndYYYYMMDD;
				checkUnique = true;
			}
		}

		if (null != newNumBatches) {
			programEntity.numBatches = newNumBatches;
		}

		if (null != newDescription)
			programEntity.description = newDescription;

		if (null != fees)
			programEntity.fee = fees;

		if (null != ccy)
			programEntity.ccy = ccy;

		ensureValid(programEntity);

		if (checkUnique) {
			ensureNotPresentInDB(client, programEntity);
			safeAddToMemcache(client, programEntity);
		}

		ofy(client).save().entity(programEntity).now();
		return programEntity.toProp();
	}

	private static void ensureValid(ProgramEntity programEntity) {
		DateUtils.ensureFormatYYYYMMDD(programEntity.startYYYYMMDD);
		DateUtils.ensureFormatYYYYMMDD(programEntity.endYYYYMMDD);

		if (programEntity.startYYYYMMDD > programEntity.endYYYYMMDD)
			throw new APIException(Status.ERROR_RESOURCE_INCORRECT,
					"Start date [" + programEntity.startYYYYMMDD
							+ "] is greater than end date ["
							+ programEntity.endYYYYMMDD + "]");

		if (programEntity.numBatches < 1)
			throw new APIException(Status.ERROR_RESOURCE_INCORRECT,
					"Invalid number of batches [" + programEntity.numBatches
							+ "]. numBatches should be positive");

		if ((programEntity.programTypeRef == null)
				|| (programEntity.venueRef == null)
				|| (programEntity.teacherRef == null)
				|| (programEntity.groupRef == null))
			throw new APIException(Status.ERROR_RESOURCE_INCORRECT,
					"Either program type or venue or teacher or group not populated");

		Utils.ensureNonNegative(programEntity.fee);

		if (programEntity.fee != 0)
			if (programEntity.ccy == null)
				Utils.throwIncorrectSpecException("Ccy should be specified");
	}

	public static ProgramEntity safeGet(String client, long programId) {
		Client.ensureValid(client);

		ProgramEntity entity = ofy(client).load().type(ProgramEntity.class)
				.id(programId).get();
		if (null == entity)
			throw new APIException(Status.ERROR_RESOURCE_NOT_FOUND,
					"There is no program with id [" + programId + "]");

		return entity;
	}

	public static ProgramEntity get(String client, long programId) {
		Client.ensureValid(client);

		ProgramEntity entity = ofy(client).load().type(ProgramEntity.class)
				.id(programId).get();

		return entity;
	}

	public static List<ProgramProp> query(String client, Integer startYYYYMMDD,
			Integer endYYYYMMDD, Long programTypeId, Set<Long> groupIds,
			Long venueId, Integer maxResultSize) {
		Client.ensureValid(client);

		Query<ProgramEntity> q = ofy(client).load().type(ProgramEntity.class);

		if (null != startYYYYMMDD)
			DateUtils.ensureFormatYYYYMMDD(startYYYYMMDD);

		if (null != endYYYYMMDD)
			DateUtils.ensureFormatYYYYMMDD(endYYYYMMDD);

		if (null != programTypeId) {
			ProgramTypeEntity programTypeEntity = ProgramType.safeGet(client,
					programTypeId);
			q = q.filter("programTypeRef", programTypeEntity);
		}

		if ((null != groupIds) && (groupIds.size() != 0)) {
			List<Key<GroupEntity>> groupKeys = new ArrayList<>();
			for (Long groupId : groupIds) {
				Key<GroupEntity> groupKey = Key.create(GroupEntity.class,
						groupId);
				groupKeys.add(groupKey);
			}

			q = q.filter("groupRef in", groupKeys);
		}

		if (null != venueId) {
			VenueEntity venueEntity = Venue.safeGet(client, venueId);
			q = q.filter("venueRef", venueEntity);
		}

		// don't limit the query as this will mess up the sort order.
		// get all results, sort and then limit the data that is returned

		List<ProgramEntity> entities = q.list();

		List<ProgramProp> props = new ArrayList<>();
		for (ProgramEntity entity : entities) {
			// discard entities that are after end date (if specified)
			if ((null != endYYYYMMDD) && (entity.endYYYYMMDD > endYYYYMMDD))
				continue;

			if ((null != startYYYYMMDD)
					&& (entity.startYYYYMMDD < startYYYYMMDD))
				continue;

			props.add(entity.toProp());
		}

		Collections.sort(props);

		if ((maxResultSize != null) && (props.size() > maxResultSize))
			props = props.subList(0, maxResultSize);

		return props;
	}

	static List<ProgramProp> getOngoingPrograms(String client,
			int dateYYYYMMDD, String login) {
		Client.ensureValid(client);
		User.ensureValidUser(client, login);

		DateUtils.ensureFormatYYYYMMDD(dateYYYYMMDD);

		Set<Long> groupIds = User.getResources(client, login,
				ResourceType.GROUP, Action.WRITE);

		// remove groupId 0 as it is not a valid group
		groupIds.remove((long) 0);

		// if user does not have write access to any group just return
		if (0 == groupIds.size())
			return new ArrayList<>();

		List<ProgramProp> programProps = query(client, null, null, null,
				groupIds, null, null);

		List<ProgramProp> ongoing = new ArrayList<>();
		for (ProgramProp programProp : programProps) {
			if ((dateYYYYMMDD >= programProp.startYYYYMMDD)
					&& (dateYYYYMMDD <= programProp.endYYYYMMDD))
				ongoing.add(programProp);
		}

		return ongoing;
	}

	public static List<SessionProp> getOngoingSessions(String client,
			int dateYYYYMMDD, String login) {
		List<ProgramProp> programProps = getOngoingPrograms(client,
				dateYYYYMMDD, login);

		List<SessionProp> sessionProps = new ArrayList<>();
		for (ProgramProp programProp : programProps) {
			sessionProps.addAll(programProp.getSessions(dateYYYYMMDD));
		}

		return sessionProps;
	}

	private static String getUniqueKey(String namespace,
			ProgramEntity programEntity) {

		return namespace + "_"
				+ programEntity.programTypeRef.get().toProp().programTypeId
				+ "_" + programEntity.venueRef.get().toProp().venueId + "_"
				+ programEntity.startYYYYMMDD + "_" + programEntity.endYYYYMMDD;
	}

	private static void ensureNotPresentInDB(String namespace,
			ProgramEntity programEntity) {
		List<Key<ProgramEntity>> keys = ofy(namespace).load()
				.type(ProgramEntity.class)
				.filter("programTypeRef", programEntity.programTypeRef)
				.filter("venueRef", programEntity.venueRef)
				.filter("startYYYYMMDD", programEntity.startYYYYMMDD)
				.filter("endYYYYMMDD", programEntity.endYYYYMMDD).keys()
				.list();

		if (keys.size() != 0)
			throw new APIException(
					Status.ERROR_RESOURCE_ALREADY_EXISTS,
					"There is already a program with the same start date, end date, venue, program type and batch.");
	}

	private static void safeAddToMemcache(String namespace,
			ProgramEntity programEntity) {
		String key = getUniqueKey(namespace, programEntity);

		long val = MemcacheServiceFactory.getMemcacheService().increment(key,
				1, (long) 0);

		if (val != 1)
			throw new APIException(
					Status.ERROR_RESOURCE_ALREADY_EXISTS,
					"There is already a program with the same start date, end date, venue, program type and batch. ");
	}
}
